# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import logging
import math
import time
from typing import Any, List, Dict, Tuple
import functools
from web3 import Web3, HTTPProvider, middleware
from dataclasses import dataclass

from concurrent.futures import ThreadPoolExecutor
from coin.exchange.uniswap_base.constants import (
    TOKEN_TO_ADDRESS,
    TOKEN_TO_DECIMAL_MULTIPLIER,
    ADDR_TO_DECIMAL_MULTIPLIER,
    balance_checker_deployment_address,
    PRODUCTION_RPC_URL,
    MAX_UINT_128,
    MAX_UINT_256,
    ROUGH_ESTIMATE_GAS_LIMIT,
    BLOCKCHAIN_NAME_TO_CHAINID,
)
from coin.exchange.uniswap_v3.kr_rest.product import UniswapProduct
from coin.exchange.uniswap_v3.kr_rest.native_private_client_utils import (
    _load_contract,
    _addr_to_str,
    _nearest_tick,
)
from coin.exchange.uniswap_v3.kr_rest.constants import (
    deployment_address,
)
from coin.exchange.uniswap_v3.kr_rest.native_private_client_utils import (
    _calculate_amount
)
from coin.exchange.util.web3.signing_util import (
    sign_and_wait,
    parse_symbol,
)

@dataclass
class TokenEntry:
    tokenId: int
    liquidity: int
    position_amount0: int
    position_amount1: int


class UniswapV3NativePrivateClientV2(object):
    def __init__(
        self,
        blockchain_name='ethereum',
        web3=None,
    ):
        self._executor = ThreadPoolExecutor(max_workers=10)
        self._blockchain_name = blockchain_name
        if web3 is not None:
            self._web3 = web3
        else:
            self._web3 = Web3(HTTPProvider(
                PRODUCTION_RPC_URL[self._blockchain_name], request_kwargs={"timeout": 30}))
            self._web3.middleware_onion.add(middleware.time_based_cache_middleware)
            self._web3.middleware_onion.add(middleware.latest_block_based_cache_middleware)
            self._web3.middleware_onion.add(middleware.simple_cache_middleware)
        self._kdeadline_sec_per_tx = 2 * 60
        self._kwithdraw_slippage_rate = 0.8

    def _quoter(self):
        return _load_contract(
            self._web3,
            "quoter",
            deployment_address["Quoter"],
        )
    quoter = property(fget=_quoter, fset=None, fdel=None, doc=None)

    def _router(self):
        return _load_contract(
            self._web3,
            "router",
            deployment_address["SwapRouter"],
        )
    router = property(fget=_router, fset=None, fdel=None, doc=None)

    def _nonFungiblePositionManager(self):
        return _load_contract(
            self._web3,
            "nonFungiblePositionManager",
            deployment_address["NonfungiblePositionManager"],
        )
    nonFungiblePositionManager = property(
        fget=_nonFungiblePositionManager, fset=None, fdel=None, doc=None
    )

    def _addr_balance_checker(self):
        net_name = self._blockchain_name
        if net_name == 'ethereum':
            net_name = 'mainnet'
        return _load_contract(
            self._web3,
            "balanceChecker",
            balance_checker_deployment_address[net_name],
        )
    addr_balance_checker = property(
        fget=_addr_balance_checker, fset=None, fdel=None, doc=None
    )

    def _factory_contract(self):
        return _load_contract(
            self._web3,
            "factory",
            deployment_address["UniswapV3Factory"],
        )
    factory_contract = property(fget=_factory_contract, fset=None, fdel=None, doc=None)

    def update_nft_token_positions(
        self,
        wallet_address: str,
    ):
        nft_manager = self.nonFungiblePositionManager
        number_of_positions = nft_manager.functions.balanceOf(wallet_address).call()
        nft_tokens = [
            nft_manager.functions.tokenOfOwnerByIndex(wallet_address, idx).call()
            for idx in range(number_of_positions)
        ]
        return nft_tokens

    @functools.lru_cache()
    def _get_pool_addr(self, token0, token1, fee):
        return self.factory_contract.functions.getPool(token0, token1, fee).call()

    def calculate_amount(self, tick_lower, tick_upper, liquidity, pool_addr):
        curr_tick = (
            _load_contract(self._web3, "pool", pool_addr).functions.slot0().call()[1]
        )
        return _calculate_amount(tick_lower, tick_upper, curr_tick, liquidity)

    @functools.lru_cache()
    def _coin_addr_to_symbol(self, token_addr):
        token_erc20_contract = _load_contract(
            self._web3, "erc20", token_addr
        )
        symbol = token_erc20_contract.functions.symbol().call().upper()
        return symbol

    def get_unclaimed_fee(self, token_id, wallet_address):
        results = self.nonFungiblePositionManager.functions.collect(
            (token_id, _addr_to_str(wallet_address), MAX_UINT_128, MAX_UINT_128)).call()
        logging.info("Fee0: %s", results[0])
        logging.info("Fee1: %s", results[1])
        return results[0], results[1]

    def get_balances(
        self,
        wallet_address: str,
        currencies: List[str],
    ) -> Dict[str, Dict[str, float]]:
        currency_addresses = [TOKEN_TO_ADDRESS[self._blockchain_name][currency] for currency in currencies]
        balances = self.addr_balance_checker.functions.balances(
            [wallet_address], currency_addresses).call()

        currency_to_balance = {}
        for currency, balance in zip(currencies, balances):
            currency_to_balance[currency] = {
                'available': balance * TOKEN_TO_DECIMAL_MULTIPLIER.get(currency, 1e-18),
                'hold': 0,
            }

        nft_tokens = self.update_nft_token_positions(wallet_address)
        for nft_token in nft_tokens:
            (
                nonce,
                operator,
                token0,
                token1,
                fee,
                tick_lower,
                tick_upper,
                liquidity,
                feeGrowthInside0LastX128,
                feeGrowthInside1LastX128,
                tokensOwed0,
                tokensOwed1,
            ) = self.nonFungiblePositionManager.functions.positions(nft_token).call()
            pool_addr = self._get_pool_addr(token0, token1, fee)
            x, y = self.calculate_amount(tick_lower, tick_upper, liquidity, pool_addr)

            symbol0 = self._coin_addr_to_symbol(token0)
            if symbol0 not in currency_to_balance:
                continue

            symbol1 = self._coin_addr_to_symbol(token1)
            if symbol1 not in currency_to_balance:
                continue

            logging.info('underlying: %s, %s' % (x * ADDR_TO_DECIMAL_MULTIPLIER[token0], y * ADDR_TO_DECIMAL_MULTIPLIER[token1]))
            fee0, fee1 = self.get_unclaimed_fee(nft_token, wallet_address)

            currency_to_balance[symbol0]["hold"] += (x + fee0) * ADDR_TO_DECIMAL_MULTIPLIER[token0]
            currency_to_balance[symbol1]["hold"] += (y + fee1) * ADDR_TO_DECIMAL_MULTIPLIER[token1]

        return currency_to_balance

    def get_nft_token_info(
        self,
        wallet_address: str,
        symbol: str,
    ) -> Dict[str, Dict[str, Any]]:
        base_currency, quote_currency, fee = parse_symbol(symbol)
        tokenid_to_nft_info = {}
        nft_tokens = self.update_nft_token_positions(wallet_address)
        for nft_token in nft_tokens:
            (
                _nonce,
                _operator,
                _token0,
                _token1,
                _fee,
                _tick_lower,
                _tick_upper,
                _liquidity,
                _feeGrowthInside0LastX128,
                _feeGrowthInside1LastX128,
                _tokensOwed0,
                _tokensOwed1,
            ) = self.nonFungiblePositionManager.functions.positions(nft_token).call()
            # check if the nft_token belongs to symbol
            if not {
                       int(TOKEN_TO_ADDRESS[self._blockchain_name][base_currency], base=16),
                       int(TOKEN_TO_ADDRESS[self._blockchain_name][quote_currency], base=16),
                       fee
                   } == {
                       int(_token0, base=16),
                       int(_token1, base=16),
                       _fee}:
                continue
            else:
                tokenid_to_nft_info[str(nft_token)] = {
                    "token0_address": _token0,
                    "token1_address": _token1,
                    "fee": _fee,
                    "tick_lower": _tick_lower,
                    "tick_upper": _tick_upper,
                    "liquidity": str(_liquidity),
                }
        return tokenid_to_nft_info

    def submit_order(
        self,
        wallet_address: str,
        symbol: str,
        price: float,
        qty: float,
        side: str,
    ) -> Tuple[bool, str or None, float or None]:
        price = float(price)
        qty = float(qty)

        logging.info("[Native client]submit_order started")
        base_currency, quote_currency, fee = parse_symbol(symbol)

        base_currency_address = TOKEN_TO_ADDRESS[self._blockchain_name][base_currency]
        quote_currency_address = TOKEN_TO_ADDRESS[self._blockchain_name][quote_currency]

        deadline = int(time.time() + 120)

        args, func, tx_param = None, None, {
            "nonce": None,
            "from": wallet_address,
            "type": "0x2",
            "maxFeePerGas": None,
            "maxPriorityFeePerGas": None,
            "gas": ROUGH_ESTIMATE_GAS_LIMIT,
            "chainId": BLOCKCHAIN_NAME_TO_CHAINID[self._blockchain_name],
        }
        match side.upper():
            case 'BUY':
                amount_out = qty
                amount_in_maximum = qty * price

                args = (
                    quote_currency_address,
                    base_currency_address,
                    fee,
                    wallet_address,
                    deadline,
                    int(amount_out / ADDR_TO_DECIMAL_MULTIPLIER[base_currency_address]),
                    int(amount_in_maximum / ADDR_TO_DECIMAL_MULTIPLIER[quote_currency_address]),
                    0,  # sqrtPriceLimit
                )

                func = self.router.functions.exactOutputSingle
            case 'SELL':
                amount_in = qty
                amount_out_minimum = qty * price

                args = (
                    base_currency_address,
                    quote_currency_address,
                    fee,
                    wallet_address,
                    deadline,
                    int(amount_in / ADDR_TO_DECIMAL_MULTIPLIER[base_currency_address]),
                    int(amount_out_minimum / ADDR_TO_DECIMAL_MULTIPLIER[quote_currency_address]),
                    0,
                )
                func = self.router.functions.exactInputSingle
            case _:
                raise NotImplementedError()
        logging.info("[Native client]build_transaction started")
        tx_param = func(args).build_transaction(
            {
                "from": wallet_address,
            }
        ) | tx_param
        logging.info('before sign: %s' % tx_param)
        return sign_and_wait(self._web3, [tx_param], symbol, self._blockchain_name)

    def is_closed_liquidity(
        self,
        liquidity: int, 
        amount0Min: int, 
        amount1Min: int
    ) -> bool:
        return liquidity <= 0 and amount0Min <= 0 and amount1Min <= 0

    def burn_closed_liquidity(
        self,
        wallet_address: str,
        tokenIds: List[int],
        tx_param: Dict[str, Any]
    ) -> Tuple[bool, str or None]:
        if tokenIds is None or len(tokenIds) <= 0:
            return (True, None)

        tokenIds.sort(reverse=True)
        overall_tx_result = True
        succeeded_tx_hash = ''
        for tokenId in tokenIds:
            data = [self.nonFungiblePositionManager.encodeABI(
                fn_name='burn', args=[tokenId])]
            tx_param_built = self.nonFungiblePositionManager.functions.multicall(data).build_transaction(
                {
                    "from": wallet_address,
                }) | tx_param
            logging.info('burn_closed_liquidity:%d' % tokenId)
            logging.info('before sign: %s' % tx_param_built)
            tx_result, tx_msg, fill_price = sign_and_wait(
                self._web3, [tx_param_built], None, self._blockchain_name)
            overall_tx_result = overall_tx_result and tx_result
            if tx_result:
                succeeded_tx_hash += (' ,' if len(succeeded_tx_hash) > 0 else '') + tx_msg
        return overall_tx_result, succeeded_tx_hash

    def withdraw_liquidity_for_token(
        self,
        wallet_address: str,
        token_entries: List[TokenEntry],
        tx_param: Dict[str, Any]
    ) -> Tuple[bool, str or None]:
        if token_entries is None or len(token_entries) <= 0:
            return (True, None)
        
        token_entries.sort(key=lambda x: x.tokenId, reverse=True)
        overall_tx_result = True
        succeeded_tx_hash = ''
        for token_entry in token_entries:
            deadline = int(time.time()) + self._kdeadline_sec_per_tx
            data = [self.nonFungiblePositionManager.encodeABI(
                fn_name='decreaseLiquidity',
                args=[[token_entry.tokenId, token_entry.liquidity, int(token_entry.position_amount0 * self._kwithdraw_slippage_rate), int(token_entry.position_amount1 * self._kwithdraw_slippage_rate), deadline]])]
            # collect
            data += [self.nonFungiblePositionManager.encodeABI(
                fn_name='collect',
                args=[[token_entry.tokenId, _addr_to_str(wallet_address), MAX_UINT_128, MAX_UINT_128]])]
            # burn
            data += [self.nonFungiblePositionManager.encodeABI(
                fn_name='burn', args=[token_entry.tokenId])]
            tx_param_built = self.nonFungiblePositionManager.functions.multicall(data).build_transaction(
                {
                    "from": wallet_address,
                }) | tx_param
            logging.info('withdraw liquidity: %d' % token_entry.tokenId)
            logging.info('before sign: %s' % tx_param_built)
            tx_result, tx_msg, fill_price = sign_and_wait(
                self._web3, [tx_param_built], None, self._blockchain_name)
            overall_tx_result = overall_tx_result and tx_result
            if tx_result:
                succeeded_tx_hash += (' ,' if len(succeeded_tx_hash) > 0 else '') + tx_msg
        return overall_tx_result, succeeded_tx_hash

    def withdraw_liquidity(
        self,
        wallet_address: str,
        symbol: str,
    ) -> Tuple[bool, str or None]:
        """
        remove all liquidity from the position associated w/ tokenId, collect fees, and burn token.
        """
        number_of_positions = self.nonFungiblePositionManager.functions.balanceOf(
            wallet_address).call()
        tx_param = {
            "nonce": None,
            "from": wallet_address,
            "type": "0x2",
            "maxFeePerGas": None,
            "maxPriorityFeePerGas": None,
            "gas": ROUGH_ESTIMATE_GAS_LIMIT,
            "chainId": BLOCKCHAIN_NAME_TO_CHAINID[self._blockchain_name],
        }
        base_currency, quote_currency, fee = parse_symbol(symbol)

        logging.info("number_of_positions:%d", number_of_positions)
        closed_tokenIds = []
        open_token_entries = []
        for idx in range(number_of_positions):
                _tokenId = self.nonFungiblePositionManager.functions.tokenOfOwnerByIndex(wallet_address, idx).call()
                (
                    _nonce,
                    _operator,
                    _token0,
                    _token1,
                    _fee,
                    _tick_lower,
                    _tick_upper,
                    _liquidity,
                    _feeGrowthInside0LastX128,
                    _feeGrowthInside1LastX128,
                    _tokensOwed0,
                    _tokensOwed1,
                ) = self.nonFungiblePositionManager.functions.positions(_tokenId).call()
                # check if the nft_token belongs to symbol
                if not {
                        int(TOKEN_TO_ADDRESS[self._blockchain_name][base_currency], base=16),
                        int(TOKEN_TO_ADDRESS[self._blockchain_name][quote_currency], base=16),
                        fee
                    } == {
                        int(_token0, base=16),
                        int(_token1, base=16),
                        _fee}:
                    continue
                else:
                    tokenId = _tokenId
                    liquidity = _liquidity
                    fee_amount0, fee_amount1 = self.nonFungiblePositionManager.functions.collect(
                        (tokenId, _addr_to_str(wallet_address), MAX_UINT_128, MAX_UINT_128)).call()
                    pool_addr = self._get_pool_addr(_token0, _token1, fee)
                    position_amount0, position_amount1 = self.calculate_amount(_tick_lower, _tick_upper, liquidity, pool_addr)
                    logging.info("tokenId:%s, liquidity:%s, fee_amount0:%s, fee_amount1:%s, position_amount0:%s, position_amount1:%s", tokenId, liquidity, fee_amount0, fee_amount1, position_amount0, position_amount1)
                    if self.is_closed_liquidity(_liquidity, fee_amount0, fee_amount1):
                        closed_tokenIds.append(_tokenId)
                    else:
                        token_entry = TokenEntry(tokenId=_tokenId, liquidity=_liquidity, position_amount0=position_amount0, position_amount1=position_amount1)
                        open_token_entries.append(token_entry)

        burn_result, burn_msg = self.burn_closed_liquidity(wallet_address, closed_tokenIds, tx_param)
        if not burn_result:
            return burn_result, burn_msg
        
        if open_token_entries is None or len(open_token_entries) <= 0:
            logging.info("[Native client Error]withdraw_liquidity: No open token!")
            return False, None
        
        return self.withdraw_liquidity_for_token(wallet_address, open_token_entries, tx_param)

    def provide_liquidity(
        self,
        wallet_address: str,
        symbol: str,
        min_price: float,
        max_price: float,
    ) -> Tuple[bool, str or None, float or None]:
        deadline = int(time.time()) + 2 * 60
        tolerance = 0.03

        base_currency, quote_currency, fee = parse_symbol(symbol)
        base_addr, quote_addr = TOKEN_TO_ADDRESS[self._blockchain_name][base_currency], TOKEN_TO_ADDRESS[self._blockchain_name][quote_currency]
        pool_addr = self.factory_contract.functions.getPool(base_addr, quote_addr, fee).call()
        pool_contract = _load_contract(self._web3, "pool", pool_addr)
        slot0 = pool_contract.functions.slot0().call()
        token0 = pool_contract.functions.token0().call()
        token1 = pool_contract.functions.token1().call()

        if (token0, token1) == (base_addr, quote_addr):
            is_reverse = False
        elif (token0, token1) == (quote_addr, base_addr):
            is_reverse = True
        else:
            raise ValueError(symbol)

        curr_price_native = ((1.0 * slot0[0] / (2 ** 96)) ** 2)
        _min_price = 1 / max_price if is_reverse else min_price
        max_price = 1 / min_price if is_reverse else max_price
        min_price = _min_price

        curr_price = curr_price_native / ADDR_TO_DECIMAL_MULTIPLIER[token0] * ADDR_TO_DECIMAL_MULTIPLIER[token1]
        logging.info('provide liquidity %s current price: %s' % (symbol, curr_price))
        curr_tick = slot0[1]

        tick_lower = curr_tick + math.log(min_price / curr_price, 1.0001)
        tick_upper = curr_tick + math.log(max_price / curr_price, 1.0001)
        tick_lower, tick_upper = _nearest_tick(tick_lower, fee), _nearest_tick(tick_upper, fee)

        tx_param = {
            "nonce": None,
            "from": wallet_address,
            "type": "0x2",
            "maxFeePerGas": None,
            "maxPriorityFeePerGas": None,
            "gas": ROUGH_ESTIMATE_GAS_LIMIT,
            "chainId": BLOCKCHAIN_NAME_TO_CHAINID[self._blockchain_name],
        }

        # First adjust balance.
        initial_amount_0, initial_amount_1 = self.addr_balance_checker.functions.balances(
            [wallet_address], [token0, token1]).call()

        if not initial_amount_1 == 0:
            initial_ratio: float = initial_amount_0 / initial_amount_1
        else:
            initial_ratio: float = float('inf')

        _x, _y = _calculate_amount(tick_lower, tick_upper, curr_tick, int(1e10))
        if not _y == 0:
            target_ratio: float = _x / _y
        else:
            target_ratio: float = float('inf')

        iter_limit: int = 20
        if math.isclose(initial_ratio, target_ratio, rel_tol=tolerance):
            pass
        elif initial_ratio > target_ratio:
            # trade token0 for token1
            low = 0.0
            high = float(initial_amount_0)
            in_adjust: int = 0
            while low < high and iter_limit > 0:
                iter_limit -= 1
                mid: int = int((low + high) / 2)
                in_adjust = mid
                out_adjust: int = self.quoter.functions.quoteExactInputSingle(
                    token0, token1, fee, mid, 0).call()  # not setting price limit
                new_ratio: float = (initial_amount_0 - in_adjust) / (initial_amount_1 + out_adjust)
                if math.isclose(new_ratio, target_ratio, rel_tol=tolerance):
                    break
                elif new_ratio > target_ratio:
                    low = mid
                else:
                    high = mid
            tx_param = self.router.functions.exactInputSingle(
                (
                    token0,
                    token1,
                    fee,
                    wallet_address,
                    deadline,
                    in_adjust,
                    0,
                    0,  # sqrtPriceLimit
                )).build_transaction({"from": wallet_address}) | tx_param
            logging.info('before sign: %s' % tx_param)
            ret = sign_and_wait(self._web3, [tx_param], symbol, self._blockchain_name)
            if not ret[0]:
                return ret
        else:
            # trade token1 for token0
            low = 0.0
            high = float(initial_amount_1)
            in_adjust: int = 0
            while low < high and iter_limit > 0:
                iter_limit -= 1
                mid: int = int((low + high) / 2)
                in_adjust = mid
                out_adjust: int = self.quoter.functions.quoteExactInputSingle(
                    token1, token0, fee, mid, 0).call()  # not setting price limit
                new_ratio: float = (initial_amount_0 + out_adjust) / (initial_amount_1 - in_adjust)
                if math.isclose(new_ratio, target_ratio, rel_tol=tolerance):
                    break
                elif new_ratio > target_ratio:
                    high = mid
                else:
                    low = mid
            tx_param = self.router.functions.exactInputSingle(
                (
                    token1,
                    token0,
                    fee,
                    wallet_address,
                    deadline,
                    in_adjust,
                    0,
                    0,  # sqrtPriceLimit
                )).build_transaction({"from": wallet_address}) | tx_param
            logging.info('before sign: %s' % tx_param)
            ret = sign_and_wait(self._web3, [tx_param], symbol, self._blockchain_name)
            if not ret[0]:
                return ret

        # Second mint liquidity.
        balance0, balance1 = self.addr_balance_checker.functions.balances(
            [wallet_address], [token0, token1]).call()
        tx_param = {
            "nonce": None,
            "from": wallet_address,
            "type": "0x2",
            "maxFeePerGas": None,
            "maxPriorityFeePerGas": None,
            "gas": ROUGH_ESTIMATE_GAS_LIMIT,
            "chainId": BLOCKCHAIN_NAME_TO_CHAINID[self._blockchain_name],
        }
        tx_param = self.nonFungiblePositionManager.functions.mint(
            (
                token0,
                token1,
                fee,
                tick_lower,
                tick_upper,
                balance0,
                balance1,
                0,
                0,
                wallet_address,
                deadline
            )).build_transaction({"from": wallet_address}) | tx_param
        logging.info('before sign: %s' % tx_param)
        return sign_and_wait(self._web3, [tx_param], None, self._blockchain_name)

    def approve_submit_order(
        self,
        wallet_address: str,
        symbol: str,
    ) -> bool:
        return self.approve_SwapRouter(wallet_address, symbol)

    def approve_SwapRouter(
        self,
        wallet_address: str,
        symbol: str,
        amount=MAX_UINT_256,
    ):
        p = UniswapProduct.FromStr(symbol)
        currencies = []
        if p.base.currency == 'ETH':
            currencies.append('WETH')
        else:
            currencies.append(p.base.currency)
        if p.quote.currency == 'ETH':
            currencies.append('WETH')
        else:
            currencies.append(p.quote.currency)
        contracts = []
        for currency in currencies:
            if currency == 'USDT':
                contract_addr = TOKEN_TO_ADDRESS[self._blockchain_name]['USDT']
                contract = _load_contract(self._web3, "usdt", Web3.to_checksum_address(contract_addr))
            else:
                contract_addr = TOKEN_TO_ADDRESS[self._blockchain_name][currency]
                contract = _load_contract(self._web3, "erc20", Web3.to_checksum_address(contract_addr))
            contracts.append(contract)

        for contract in contracts:
            tx_param = contract.functions.approve(
                deployment_address["SwapRouter"], amount).build_transaction(
                {
                    "from": wallet_address,
                    "type": "0x2",
                    "maxFeePerGas": self._web3.eth.max_priority_fee + (
                        2 * self._web3.eth.get_block("latest")["baseFeePerGas"]),
                    "maxPriorityFeePerGas": self._web3.eth.max_priority_fee,
                    "chainId": BLOCKCHAIN_NAME_TO_CHAINID[self._blockchain_name],
                }
            )
            tx_param["gas"] = int(self._web3.eth.estimate_gas(tx_param) * 1.2)
            logging.info(tx_param)
            ret = sign_and_wait(self._web3, [tx_param], None, self._blockchain_name)
            if not ret[0]:
                return False
        return True

    def approve_provide_liquidity(
        self,
        wallet_address: str,
        symbol: str,
    ) -> bool:
        return self.approve_NonfungiblePositionManager(wallet_address, symbol)

    def approve_NonfungiblePositionManager(
        self,
        wallet_address: str,
        symbol: str,
        amount=MAX_UINT_256,
    ) -> bool:
        p = UniswapProduct.FromStr(symbol)
        currencies = []
        if p.base.currency == 'ETH':
            currencies.append('WETH')
        else:
            currencies.append(p.base.currency)
        if p.quote.currency == 'ETH':
            currencies.append('WETH')
        else:
            currencies.append(p.quote.currency)
        contracts = []
        for currency in currencies:
            if currency == 'USDT':
                contract_addr = TOKEN_TO_ADDRESS[self._blockchain_name]['USDT']
                contract = _load_contract(self._web3, "usdt", Web3.to_checksum_address(contract_addr))
            else:
                contract_addr = TOKEN_TO_ADDRESS[self._blockchain_name][currency]
                contract = _load_contract(self._web3, "erc20", Web3.to_checksum_address(contract_addr))
            contracts.append(contract)

        for contract in contracts:
            tx_param = contract.functions.approve(
                deployment_address["NonfungiblePositionManager"], amount).build_transaction(
                {
                    "from": wallet_address,
                    "type": "0x2",
                    "maxFeePerGas": self._web3.eth.max_priority_fee + (
                        2 * self._web3.eth.get_block("latest")["baseFeePerGas"]),
                    "maxPriorityFeePerGas": self._web3.eth.max_priority_fee,
                    "chainId": BLOCKCHAIN_NAME_TO_CHAINID[self._blockchain_name],
                }
            )
            tx_param["gas"] = int(self._web3.eth.estimate_gas(tx_param) * 1.2)
            logging.info(tx_param)
            ret = sign_and_wait(self._web3, [tx_param], None, self._blockchain_name)
            if not ret[0]:
                return False
        return True
